// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <threads.h>
#include <unistd.h>
#include <zircon/compiler.h>
#include <zircon/syscalls.h>

#include <unittest/unittest.h>

static bool wait(zx_handle_t event, zx_handle_t quit_event) {
  zx_status_t ms;
  zx_wait_item_t items[2];
  items[0].waitfor = ZX_EVENT_SIGNALED;
  items[0].handle = event;
  items[1].waitfor = ZX_EVENT_SIGNALED;
  items[1].handle = quit_event;

  ms = zx_object_wait_many(items, 2, ZX_TIME_INFINITE);
  if (ms < 0)
    return false;

  return (items[1].pending & ZX_EVENT_SIGNALED);
}

static bool wait_user(zx_handle_t event, zx_handle_t quit_event, zx_signals_t user_signal) {
  zx_status_t ms;

  zx_wait_item_t items[2];
  items[0].waitfor = user_signal;
  items[0].handle = event;
  items[1].waitfor = ZX_EVENT_SIGNALED;
  items[1].handle = quit_event;

  ms = zx_object_wait_many(items, 2, ZX_TIME_INFINITE);
  if (ms < 0)
    return false;

  return (items[1].pending & ZX_EVENT_SIGNALED);
}

static int thread_fn_1(void* arg) {
  zx_handle_t* events = (zx_handle_t*)(arg);

  do {
    zx_nanosleep(zx_deadline_after(ZX_MSEC(200)));
    __UNUSED zx_status_t status = zx_object_signal(events[1], 0u, ZX_EVENT_SIGNALED);
    assert(status == ZX_OK);
  } while (!wait(events[2], events[0]));

  return 0;
}

static int thread_fn_2(void* arg) {
  zx_handle_t* events = (zx_handle_t*)(arg);

  while (!wait(events[1], events[0])) {
    zx_nanosleep(zx_deadline_after(ZX_MSEC(100)));
    __UNUSED zx_status_t status = zx_object_signal(events[2], 0u, ZX_EVENT_SIGNALED);
    assert(status == ZX_OK);
  }

  return 0;
}

static bool basic_test(void) {
  BEGIN_TEST;

  zx_handle_t events[3];
  ASSERT_EQ(zx_event_create(0u, &events[0]), 0, "Error during event create");
  ASSERT_EQ(zx_event_create(0u, &events[1]), 0, "Error during event create");
  ASSERT_EQ(zx_event_create(0u, &events[2]), 0, "Error during event create");

  thrd_t threads[4];
  int ret = thrd_create_with_name(&threads[3], thread_fn_1, events, "master");
  ASSERT_EQ(ret, thrd_success, "Error during thread creation");

  for (int ix = 0; ix != 3; ++ix) {
    ret = thrd_create_with_name(&threads[ix], thread_fn_2, events, "worker");
    ASSERT_EQ(ret, thrd_success, "Error during thread creation");
  }

  zx_nanosleep(zx_deadline_after(ZX_MSEC(400)));
  zx_object_signal(events[0], 0u, ZX_EVENT_SIGNALED);

  for (int ix = 0; ix != 4; ++ix) {
    ASSERT_EQ(thrd_join(threads[ix], NULL), thrd_success, "Error during wait");
  }

  ASSERT_GE(zx_handle_close(events[0]), 0, "Error during event-0 close");
  ASSERT_GE(zx_handle_close(events[1]), 0, "Error during event-1 close");
  ASSERT_GE(zx_handle_close(events[2]), 0, "Error during event-2 close");
  END_TEST;
}

static int thread_fn_3(void* arg) {
  zx_handle_t* events = (zx_handle_t*)(arg);

  do {
    zx_nanosleep(zx_deadline_after(ZX_MSEC(200)));
    zx_object_signal(events[1], ZX_USER_SIGNAL_ALL, ZX_USER_SIGNAL_1);
  } while (!wait_user(events[2], events[0], ZX_USER_SIGNAL_2));

  return 0;
}

static int thread_fn_4(void* arg) {
  zx_handle_t* events = (zx_handle_t*)(arg);

  while (!wait_user(events[1], events[0], ZX_USER_SIGNAL_1)) {
    zx_nanosleep(zx_deadline_after(ZX_MSEC(100)));
    zx_object_signal(events[2], ZX_USER_SIGNAL_ALL, ZX_USER_SIGNAL_2);
  }

  return 0;
}

static bool user_signals_test(void) {
  BEGIN_TEST;

  zx_handle_t events[3];
  ASSERT_GE(zx_event_create(0U, &events[0]), 0, "Error during event create");
  ASSERT_GE(zx_event_create(0U, &events[1]), 0, "Error during event create");
  ASSERT_GE(zx_event_create(0U, &events[2]), 0, "Error during event create");

  thrd_t threads[4];
  int ret = thrd_create_with_name(&threads[3], thread_fn_3, events, "master");
  ASSERT_EQ(ret, thrd_success, "Error during thread creation");

  for (int ix = 0; ix != 3; ++ix) {
    ret = thrd_create_with_name(&threads[ix], thread_fn_4, events, "workers");
    ASSERT_EQ(ret, thrd_success, "Error during thread creation");
  }

  zx_nanosleep(zx_deadline_after(ZX_MSEC(400)));
  zx_object_signal(events[0], 0u, ZX_EVENT_SIGNALED);

  for (int ix = 0; ix != 4; ++ix) {
    ASSERT_EQ(thrd_join(threads[ix], NULL), thrd_success, "Error during wait");
  }

  ASSERT_GE(zx_handle_close(events[0]), 0, "Error during event-0 close");
  ASSERT_GE(zx_handle_close(events[1]), 0, "Error during event-1 close");
  ASSERT_GE(zx_handle_close(events[2]), 0, "Error during event-2 close");
  END_TEST;
}

static int thread_fn_closer(void* arg) {
  zx_nanosleep(zx_deadline_after(ZX_MSEC(200)));

  zx_handle_t handle = *((zx_handle_t*)arg);
  int rc = (int)zx_handle_close(handle);

  return rc;
}

static bool wait_signals_test(void) {
  BEGIN_TEST;

  zx_handle_t events[3];
  ASSERT_EQ(zx_event_create(0U, &events[0]), 0, "Error during event create");
  ASSERT_EQ(zx_event_create(0U, &events[1]), 0, "Error during event create");
  ASSERT_EQ(zx_event_create(0U, &events[2]), 0, "Error during event create");

  zx_status_t status;
  zx_signals_t pending;

  zx_wait_item_t items[3];
  items[0].waitfor = ZX_EVENT_SIGNALED;
  items[0].handle = events[0];
  items[1].waitfor = ZX_EVENT_SIGNALED;
  items[1].handle = events[1];
  items[2].waitfor = ZX_EVENT_SIGNALED;
  items[2].handle = events[2];

  status = zx_object_wait_one(events[0], ZX_EVENT_SIGNALED, zx_deadline_after(1u), &pending);
  ASSERT_EQ(status, ZX_ERR_TIMED_OUT, "wait should have timeout");
  ASSERT_EQ(pending, 0u, "");

  status = zx_object_wait_many(items, 3, zx_deadline_after(1));
  ASSERT_EQ(status, ZX_ERR_TIMED_OUT, "wait should have timeout");
  ASSERT_EQ(items[0].pending, 0u, "");
  ASSERT_EQ(items[1].pending, 0u, "");
  ASSERT_EQ(items[2].pending, 0u, "");

  status = zx_object_wait_one(events[0], ZX_EVENT_SIGNALED, 0u, &pending);
  ASSERT_EQ(status, ZX_ERR_TIMED_OUT, "wait should have timeout");
  ASSERT_EQ(pending, 0u, "");

  status = zx_object_wait_many(items, 3, 0);
  ASSERT_EQ(status, ZX_ERR_TIMED_OUT, "wait should have timeout");
  ASSERT_EQ(items[0].pending, 0u, "");
  ASSERT_EQ(items[1].pending, 0u, "");
  ASSERT_EQ(items[2].pending, 0u, "");

  ASSERT_GE(zx_object_signal(events[0], 0u, ZX_EVENT_SIGNALED), 0, "Error during event signal");

  status = zx_object_wait_one(events[0], ZX_EVENT_SIGNALED, zx_deadline_after(1u), &pending);
  ASSERT_EQ(status, 0, "wait failed");
  ASSERT_EQ(pending, ZX_EVENT_SIGNALED, "Error during wait call");

  status = zx_object_wait_many(items, 3, zx_deadline_after(1));
  ASSERT_EQ(status, 0, "wait failed");
  ASSERT_EQ(items[0].pending, ZX_EVENT_SIGNALED, "Error during wait call");

  status = zx_object_wait_one(events[0], ZX_EVENT_SIGNALED, 0u, &pending);
  ASSERT_EQ(status, ZX_OK, "wait failed");
  ASSERT_EQ(pending, ZX_EVENT_SIGNALED, "Error during wait call");

  ASSERT_GE(zx_object_signal(events[0], ZX_EVENT_SIGNALED, 0u), 0, "Error during event reset");
  ASSERT_GE(zx_object_signal(events[2], 0u, ZX_EVENT_SIGNALED), 0, "Error during event signal");
  status = zx_object_wait_many(items, 3, zx_deadline_after(1));
  ASSERT_EQ(status, 0, "wait failed");
  ASSERT_EQ(items[2].pending, ZX_EVENT_SIGNALED, "Error during wait call");

  thrd_t thread;
  int ret = thrd_create_with_name(&thread, thread_fn_closer, &events[1], "closer");
  ASSERT_EQ(ret, thrd_success, "Error during thread creation");

  status = zx_object_wait_one(events[1], ZX_EVENT_SIGNALED, ZX_TIME_INFINITE, NULL);
  ASSERT_EQ(status, ZX_ERR_CANCELED, "Error during wait");

  ASSERT_EQ(thrd_join(thread, NULL), thrd_success, "Error during thread close");

  ASSERT_GE(zx_handle_close(events[0]), 0, "Error during event-0 close");
  ASSERT_GE(zx_handle_close(events[2]), 0, "Error during event-2 close");

  END_TEST;
}

static bool reset_test(void) {
  BEGIN_TEST;
  zx_handle_t event;
  ASSERT_EQ(zx_event_create(0U, &event), 0, "Error during event creation");
  ASSERT_GE(zx_object_signal(event, 0u, ZX_EVENT_SIGNALED), 0, "Error during event signal");
  ASSERT_GE(zx_object_signal(event, ZX_EVENT_SIGNALED, 0u), 0, "Error during event reset");

  zx_status_t status;
  status = zx_object_wait_one(event, ZX_EVENT_SIGNALED, zx_deadline_after(1u), NULL);
  ASSERT_EQ(status, ZX_ERR_TIMED_OUT, "wait should have timeout");

  ASSERT_EQ(zx_handle_close(event), ZX_OK, "error during handle close");

  END_TEST;
}

static bool wait_many_failures_test(void) {
  BEGIN_TEST;

  ASSERT_EQ(zx_object_wait_many(NULL, 0, zx_deadline_after(1)), ZX_ERR_TIMED_OUT,
            "wait_many on zero handles should have timed out");

  zx_handle_t handles[2] = {ZX_HANDLE_INVALID, ZX_HANDLE_INVALID};
  ASSERT_EQ(zx_event_create(0u, &handles[0]), 0, "Error during event creation");

  zx_wait_item_t items[2];
  items[0].handle = handles[0];
  items[0].waitfor = ZX_EVENT_SIGNALED;
  items[1].handle = handles[1];
  items[1].waitfor = ZX_EVENT_SIGNALED;
  ASSERT_EQ(zx_object_wait_many(items, 2, ZX_TIME_INFINITE), ZX_ERR_BAD_HANDLE,
            "Wait-many should have failed with ZX_ERR_BAD_HANDLE");

  // Signal the event, to check that wait-many cleaned up correctly.
  ASSERT_EQ(zx_object_signal(handles[0], 0u, ZX_EVENT_SIGNALED), ZX_OK,
            "Error during event signal");

  // TODO(vtl): Also test other failure code paths: 1. a handle not supporting waiting (i.e., not
  // having a Waiter), 2. a handle having an I/O port bound.

  ASSERT_EQ(zx_handle_close(handles[0]), ZX_OK, "Error during handle close");

  END_TEST;
}

BEGIN_TEST_CASE(event_tests)
RUN_TEST(basic_test)
RUN_TEST(user_signals_test)
RUN_TEST(wait_signals_test)
RUN_TEST(reset_test)
RUN_TEST(wait_many_failures_test)
END_TEST_CASE(event_tests)
